/**
 * 
 */
package org.bookie.model;

import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.bookie.model.operator.Operator;
import org.hibernate.validator.NotNull;

/**
 * Class modelling a bet slip of an {@link User}. A bet slip contains at least
 * one {@link Wager} and a stake. The profit of the bet slip is calculated
 * depending on the stake and the betting success.
 * 
 * @author Igor Drobiazko
 * 
 */
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
		name="HIERARCHY",
		discriminatorType=DiscriminatorType.STRING
)
@Table(name="BetSlips")
public abstract class BetSlip extends CommonEntity implements IBetSlip {
	/** User this BetSlip belongs to */
    @ManyToOne
    @JoinColumn(name="USER_ID", nullable=false)
	private User user;

	/** Stake made by the User */
	@Basic @NotNull private Double stake = new Double(0);

	/** Profit of the BetSlip */
	@Basic private Double profit = new Double(0);

	/** List of Bets belonging to this BetSlip */
	@OneToMany(fetch=FetchType.LAZY, cascade={CascadeType.ALL}, mappedBy="betSlip")
	private List<Wager> bets = new ArrayList<Wager>();

	/** Date the BetSlip was placed */
	@Basic @NotNull private Date date = new Date();

	/** Constructs an empty instance of BetSlip */
	protected BetSlip() {
		super();
	}

	/**
	 * Constructs a new instance of the BetSlip from another BetSlip.
	 * 
	 * @param betSlip
	 *            BetSlip to create new one from
	 */
	public BetSlip(BetSlip betSlip) {
		this(betSlip.getUser(), betSlip.getStake());
		setProfit(betSlip.getProfit());
		for(Wager bet:betSlip.getBets()){
			addBet(bet);
		}
	}

	/**
	 * Constructs a new instance of the BetSlip.
	 * 
	 * @param user
	 *            owner of the BetSlip to set
	 * @param stake
	 *            stake to place
	 */
	public BetSlip(User user, Double stake) {
		setUser(user);
		setStake(stake);
	}

	/* (non-Javadoc)
	 * @see org.bookie.model.IBetSlip#getTotalOdds()
	 */
	public Double getTotalOdds() {
		double result = getOperator().getIdentity().doubleValue();
		Iterator iter = getBets().iterator();
		while (iter.hasNext()) {
			Wager next = (Wager) iter.next();
			result = getOperator().apply(result, next.getOdds().doubleValue());
		}
		return new Double(result);
	}

	/* (non-Javadoc)
	 * @see org.bookie.model.IBetSlip#getTotalStake()
	 */
	public Double getTotalStake() {
		return getStake();
	}

	/**
	 * Recalculates the profit of the BetSlip. This method should be called when
	 * an {@link Event} of a {@link Wager} belonging to this BetSlip got a
	 * successful {@link Wager}.
	 */
	public void recalcProfit() {
		double result = getOperator().getIdentity().doubleValue();
		for(Wager bet: getBets() ){
			double odd = 0;
			if (bet.isSuccessful()) {
				odd = bet.getOdds().doubleValue();
			}
			result = getOperator().apply(result, odd*getStake().doubleValue());
		}
		setProfit(new Double(result));
	}

	/* (non-Javadoc)
	 * @see org.bookie.model.IBetSlip#getMaximalProfit()
	 */
	public Double getMaximalProfit() {
		return new Double(getTotalOdds().doubleValue()
				* getStake().doubleValue());
	}

	/* (non-Javadoc)
	 * @see org.bookie.model.IBetSlip#isClosed()
	 */
	public boolean isClosed() {
		Iterator iter = getBets().iterator();
		while (iter.hasNext()) {
			Wager next = (Wager) iter.next();
			if (null == next.getOutcome().getBet().getSuccessfulOutcome()) {
				return false;
			}
		}
		return true;
	}
	
	public boolean isSuccessful() {
		Object found = CollectionUtils.find(getBets(), new Predicate(){
			public boolean evaluate(Object input) {
				Wager wager = (Wager) input;
				return wager.isSuccessful();
			}});
		return found!=null;
	}
	
	
	/**
	 * {@inheritDoc}
	 * @see org.bookie.model.IBetSlip#getBalance()
	 */
	public Double getBalance() {
		return new Double(getProfit().doubleValue()-getTotalStake().doubleValue());
	}

	/**
	 * Get the instance of {@link Operator} used for calculation.
	 * 
	 * @return instance of Operator
	 */
	protected abstract Operator getOperator();

	/* (non-Javadoc)
	 * @see org.bookie.model.IBetSlip#getBets()
	 */
	public List<Wager> getBets() {
		return bets;
	}

	/* (non-Javadoc)
	 * @see org.bookie.model.IBetSlip#getStake()
	 */
	public Double getStake() {
		return stake;
	}

	/* (non-Javadoc)
	 * @see org.bookie.model.IBetSlip#getUser()
	 */
	public User getUser() {
		return user;
	}

	/**
	 * Set the owner of this BetSlip
	 * 
	 * @param user User to set
	 */
	public void setUser(User user) {
		this.user = user;
	}

	/**
	 * Set the stake.
	 * 
	 * @param stake
	 *            stake to set
	 */
	public void setStake(Double stake) {
		this.stake = stake;
	}

	/* (non-Javadoc)
	 * @see org.bookie.model.IBetSlip#getProfit()
	 */
	public Double getProfit() {
		return profit;
	}

	/**
	 * Set the current profit.
	 * 
	 * @param profit
	 *            amount to set
	 */
	public void setProfit(Double profit) {
		this.profit = profit;
	}

	/**
	 * Get if the BetSlip is valid
	 * 
	 * @return <true> if valid, otherwise <false>
	 */
	public boolean isValid() {
		if (null == getUser()) {
			return false;
		}
		if (getStake().doubleValue() == 0) {
			return false;
		}
		if (getBets().size() < 1) {
			return false;
		}
		return true;
	}

	/* (non-Javadoc)
	 * @see org.bookie.model.IBetSlip#getDate()
	 */
	public Date getDate() {
		return date;
	}

	/**
	 * Set the date BetSlip was placed
	 * 
	 * @param date
	 *            date to set
	 */
	public void setDate(Date date) {
		this.date = date;
	}
	
	/**
	 * Adds a {@link Wager} to the BetSlip
	 * @param bet Bet to add
	 * @return this BetSlip
	 */
	public BetSlip addBet(Wager... bets){
		for(Wager next:bets){
			getBets().add(next);
			next.setBetSlip(this);
		}
		return this;
	}
}
